<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ImgTC | Admin</title>
  <!-- Import CSS -->
  <link rel="stylesheet" href="./admin-imgtc.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.15.3/lib/theme-chalk/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css">
  <script src="https://js.sentry-cdn.com/219f636ac7bde5edab2c3e16885cb535.min.js" crossorigin="anonymous"></script>
</head>
<body>
  <div id="app">
    <el-container>
      <el-header>
        <div class="header-content">
          <span class="title" @click="refreshDashboard">Dashboard</span>
          <div class="search-card"><el-input v-model="search" size="mini" placeholder="输入关键字搜索"></el-input></div>
          <el-tooltip content="批量上传" placement="bottom">
            <span class="stats" @click="handleUpload"><i class="fas fa-upload upload-icon"></i><span class="stats-text">记录数: </span>{{ Number }}</span>
          </el-tooltip>
          <input type="file" ref="fileInput" style="display: none;" multiple @change="uploadFiles">
          <div class="actions">
            <el-tooltip content="文件类型" placement="bottom">
              <el-dropdown @command="switchFileType" :hide-on-click="false">
                <span class="el-dropdown-link"><i :class="fileTypeIcon"></i></span>
                <el-dropdown-menu slot="dropdown">
                  <el-dropdown-item command="image" :class="{ 'el-dropdown-menu__item--selected': fileType === 'image' }"><i :class="fileConfig.image.icon"></i> 图片</el-dropdown-item>
                  <el-dropdown-item command="video" :class="{ 'el-dropdown-menu__item--selected': fileType === 'video' }"><i :class="fileConfig.video.icon"></i> 视频</el-dropdown-item>
                  <el-dropdown-item command="audio" :class="{ 'el-dropdown-menu__item--selected': fileType === 'audio' }"><i :class="fileConfig.audio.icon"></i> 音频</el-dropdown-item>
                  <el-dropdown-item command="document" :class="{ 'el-dropdown-menu__item--selected': fileType === 'document' }"><i :class="fileConfig.document.icon"></i> 文件</el-dropdown-item>
                </el-dropdown-menu>
              </el-dropdown>
            </el-tooltip>
            <el-tooltip content="排序" placement="bottom">
              <el-dropdown @command="sort" :hide-on-click="false">
                <span class="el-dropdown-link"><i :class="sortIcon"></i></span>
                <el-dropdown-menu slot="dropdown">
                  <el-dropdown-item command="dateDesc" :class="{ 'el-dropdown-menu__item--selected': sortOption === 'dateDesc' }"><i class="fas fa-sort-amount-down"></i> 按时间倒序</el-dropdown-item>
                  <el-dropdown-item command="nameAsc" :class="{ 'el-dropdown-menu__item--selected': sortOption === 'nameAsc' }"><i class="fas fa-sort-alpha-up"></i> 按名称升序</el-dropdown-item>
                  <el-dropdown-item command="sizeDesc" :class="{ 'el-dropdown-menu__item--selected': sortOption === 'sizeDesc' }"><i class="fas fa-sort-amount-down"></i> 按大小倒序</el-dropdown-item>
                </el-dropdown-menu>
              </el-dropdown>
            </el-tooltip>
            <el-tooltip content="筛选" placement="bottom">
              <el-dropdown @command="filter" :hide-on-click="false">
                <span class="el-dropdown-link"><i :class="filterIcon"></i></span>
                <el-dropdown-menu slot="dropdown">
                  <el-dropdown-item command="all" :class="{ 'el-dropdown-menu__item--selected': filterOption === 'all' }"><i class="fas fa-filter"></i> 全部</el-dropdown-item>
                  <el-dropdown-item command="favorites" :class="{ 'el-dropdown-menu__item--selected': filterOption === 'favorites' }"><i class="fas fa-bookmark"></i> 收藏</el-dropdown-item>
                  <el-dropdown-item command="blocked" :class="{ 'el-dropdown-menu__item--selected': filterOption === 'blocked' }"><i class="fas fa-lock"></i> 黑名单</el-dropdown-item>
                  <el-dropdown-item command="unblocked" :class="{ 'el-dropdown-menu__item--selected': filterOption === 'unblocked' }"><i class="fas fa-unlock"></i> 白名单</el-dropdown-item>
                  <el-dropdown-item command="adult" :class="{ 'el-dropdown-menu__item--selected': filterOption === 'adult' }"><i class="fas fa-user-secret"></i> NSFW</el-dropdown-item>
                </el-dropdown-menu>
              </el-dropdown>
            </el-tooltip>
            <el-tooltip content="批量操作" placement="bottom">
              <el-dropdown @command="handleBatchOperation" :hide-on-click="false">
                <span class="el-dropdown-link"><i class="fas fa-tasks"></i></span>
                <el-dropdown-menu slot="dropdown">
                  <el-dropdown-item command="copy" :class="{ disabled: selectedFiles.length === 0 }"><i class="fas fa-link"></i> 批量复制</el-dropdown-item>
                  <el-dropdown-item command="delete" :class="{ disabled: selectedFiles.length === 0 }"><i class="fas fa-trash-alt"></i> 批量删除</el-dropdown-item>
                  <el-dropdown-item command="download" :class="{ disabled: selectedFiles.length === 0 }"><i class="fas fa-download"></i> 批量下载</el-dropdown-item>
                  <el-dropdown-item command="block" :class="{ disabled: selectedFiles.length === 0 }"><i class="fas fa-lock"></i> 加入黑名单</el-dropdown-item>
                  <el-dropdown-item command="unblock" :class="{ disabled: selectedFiles.length === 0 }"><i class="fas fa-unlock"></i> 加入白名单</el-dropdown-item>
                </el-dropdown-menu>
              </el-dropdown>
            </el-tooltip>
            <el-tooltip content="快捷方式" placement="bottom">
              <el-dropdown @command="handleWebsite">
                <span class="el-dropdown-link"><i class="fas fa-link"></i></span>
                <el-dropdown-menu slot="dropdown">
                  <template v-for="site in quickWebsites">
                    <el-dropdown-item :command="site.url">
                      <i :class="site.icon"></i> {{ site.name }}
                    </el-dropdown-item>
                  </template>
                </el-dropdown-menu>
              </el-dropdown>
            </el-tooltip>
            <el-tooltip content="工具箱" placement="bottom">
              <el-dropdown @command="handleToolkit" :hide-on-click="false">
                <span class="el-dropdown-link"><i class="fas fa-toolbox"></i></span>
                <el-dropdown-menu slot="dropdown">
                  <el-dropdown-item command="selectAllInPage"><i class="fas fa-check-square"></i> 全选当前页</el-dropdown-item>
                  <el-dropdown-item command="checkBrokenFiles"><i class="fas fa-wrench"></i> 检测失效文件</el-dropdown-item>
                  <el-dropdown-item command="editWebsites"><i class="fas fa-edit"></i> 编辑快捷方式</el-dropdown-item>
                </el-dropdown-menu>
              </el-dropdown>
            </el-tooltip>
            <el-tooltip content="退出登录" placement="bottom"><i class="fas fa-home" @click="handleLogout"></i></el-tooltip>
          </div>
        </div>
      </el-header>
      <el-main class="main-container">
        <div class="content">
          <template v-for="(item, index) in paginatedTableData" :key="index">
            <!-- 图片 -->
            <template v-if="fileType === 'image'">
              <el-card class="image-card">
                <span class="collect-icon" @click.stop="toggleLike(index, item.name)">
                <i :class="item.metadata.liked ? 'fa-solid fa-bookmark liked' : 'fa-regular fa-bookmark not-liked'"></i>
                </span>
                <el-checkbox v-model="item.selected" :ref="'checkbox-' + index"></el-checkbox>
                <el-image :src="'/file/' + item.name" :preview-src-list="['/file/' + item.name]" fit="cover" lazy="true"></el-image>
                <div class="image-overlay">
                  <div class="overlay-buttons">
                    <el-button size="mini" type="info" @click.stop="handleEditName(item)">编辑</el-button>
                    <el-button size="mini" type="primary" @click.stop="handleCopy(index, item.name)">复制</el-button>
                    <el-button size="mini" type="danger" @click.stop="handleDelete(index, item.name)">删除</el-button>
                  </div>
              </div>
              <div class="card-footer">
                <el-popover
                  trigger="click"
                  placement="top"
                  popper-class="custom-popover">
                  <template #default>
                    <p v-html="formattedFileDetails(item)"></p>
                  </template>
                  <template #reference>
                    <span :style="{ color: item.metadata.ListType !== 'Block' ? '#fff' : '#aaa' }">
                      {{ item.metadata.fileName || item.name }}
                    </span>
                  </template>
                </el-popover>
              </div>
              </el-card>
            </template>
            <!-- 视频 -->
            <template v-else-if="fileType === 'video'">
              <el-card class="video-card" :class="{ 'selected': item.selected }">
                <div class="video-content">
                  <video :src="'/file/' + item.name" controls style="width: 100%; height: 100%; object-fit: cover;"></video>
                  <div class="video-title">
                    <el-popover
                      trigger="click"
                      placement="top"
                      popper-class="custom-popover">
                      <template #default>
                        <p v-html="formattedFileDetails(item)"></p>
                      </template>
                      <template #reference>
                        <span :style="{ color: item.metadata.ListType !== 'Block' ? '#fff' : '#aaa' }">{{ item.metadata.fileName || item.name }}</span>
                      </template>
                    </el-popover>
                  </div>
                    <!-- 控制按钮区域 -->
                  <div class="video-controls">
                    <button class="control-btn like-btn" @click.stop="toggleLike(index, item.name)"><i :class="item.metadata.liked ? 'fas fa-heart liked' : 'far fa-heart'"></i></button>
                    <button class="control-btn edit-btn" @click.stop="handleEditName(item)"><i class="fas fa-edit"></i></button>
                    <button class="control-btn select-btn" @click.stop="toggleSelect(index, item.name)"><i :class="item.selected ? 'fas fa-square-check selected' : 'far fa-square'"></i></button>
                    <button class="control-btn" @click.stop="handleCopy(index, item.name)"><i class="fas fa-link"></i></button>
                    <button class="control-btn" @click.stop="handleDelete(index, item.name)"><i class="fas fa-trash-alt"></i></button>
                  </div>
                </div>
              </el-card>
            </template>
            <!-- 音频 -->
            <template v-else-if="fileType === 'audio'">
              <el-card class="audio-card" :class="{ 'selected': item.selected }">
                <div class="audio-content">
                  <!-- 音频标题区域 -->
                  <div class="audio-header">
                    <div class="audio-avatar">
                      <img src="./music.svg" alt="Music">
                    </div>
                    <div class="audio-info">
                      <div class="audio-title">
                        <el-popover
                          trigger="click"
                          placement="top"
                          popper-class="custom-popover">
                          <template #default>
                            <p v-html="formattedFileDetails(item)"></p>
                          </template>
                          <template #reference>
                            <span :style="{ color: item.metadata.ListType !== 'Block' ? '#fff' : '#aaa' }">{{ item.metadata.fileName || item.name }}</span>
                          </template>
                        </el-popover>
                      </div>
                      <div class="audio-subtitle">{{ getFileType(item.name) }}</div>
                    </div>
                  </div>
                  <audio
                    class="custom-audio-player"
                    :src="'/file/' + item.name"
                    controls
                    preload="metadata">
                    当前浏览器不支持音频播放
                  </audio>
                  <!-- 控制按钮区域 -->
                  <div class="audio-controls">
                    <button class="control-btn like-btn" @click.stop="toggleLike(index, item.name)"><i :class="item.metadata.liked ? 'fas fa-heart liked' : 'far fa-heart'"></i></button>
                    <button class="control-btn edit-btn" @click.stop="handleEditName(item)"><i class="fas fa-edit"></i></button>
                    <button class="control-btn select-btn" @click.stop="toggleSelect(index, item.name)"><i :class="item.selected ? 'fas fa-square-check selected' : 'far fa-square'"></i></button>
                    <button class="control-btn" @click.stop="handleCopy(index, item.name)"><i class="fas fa-copy"></i></button>
                    <button class="control-btn" @click.stop="handleDelete(index, item.name)"><i class="fas fa-trash-alt"></i></button>
                  </div>
                </div>
              </el-card>
            </template>
            <!-- 文件 -->
            <template v-else>
              <el-card class="file-card" :class="{ 'selected': item.selected }">
                <div class="file-content">
                  <!-- 文件标题区域 -->
                  <div class="file-header">
                    <div class="file-avatar">
                      <i :class="getFileIcon(item.name)" style="font-size: 32px;"></i>
                    </div>
                    <div class="file-info">
                      <div class="file-title">
                        <el-popover
                          trigger="click"
                          placement="top"
                          popper-class="custom-popover">
                          <template #default>
                            <p v-html="formattedFileDetails(item)"></p>
                          </template>
                          <template #reference>
                            <span :style="{ color: item.metadata.ListType !== 'Block' ? '#fff' : '#aaa' }">{{ item.metadata.fileName || item.name }}</span>
                          </template>
                        </el-popover>
                      </div>
                      <div class="file-subtitle">{{ getFileType(item.name) }}</div>
                    </div>
                  </div>
                  <!-- 控制按钮区域 -->
                  <div class="file-controls">
                    <button class="control-btn like-btn" @click.stop="toggleLike(index, item.name)"><i :class="item.metadata.liked ? 'fas fa-heart liked' : 'far fa-heart'"></i></button>
                    <button class="control-btn edit-btn" @click.stop="handleEditName(item)"><i class="fas fa-edit"></i></button>
                    <button class="control-btn select-btn" @click.stop="toggleSelect(index, item.name)"><i :class="item.selected ? 'fas fa-square-check selected' : 'far fa-square'"></i></button>
                    <button class="control-btn" @click.stop="handleCopy(index, item.name)"><i class="fas fa-copy"></i></button>
                    <button class="control-btn" @click.stop="handleDelete(index, item.name)"><i class="fas fa-trash-alt"></i></button>
                  </div>
                </div>
              </el-card>
            </template>
          </template>
        </div>
  <div class="pagination-container">
    <el-pagination
      background layout="prev, pager, next"
      :total="filteredTableData.length" :page-size="pageSize"
      @current-change="handlePageChange" :current-page.sync="currentPage" />
  </div>
  <div style="text-align:center;margin-top:16px;">
    <el-button v-if="nextCursor" @click="loadMore">加载更多</el-button>
  </div>
  <el-footer class="footer">
          <div>Powered By</div>
          <a href="https://github.com/cf-pages/Telegraph-Image" target="_blank" rel="noopener noreferrer">
            <div><i class="fa-brands fa-github"></i> Telegraph-Image</div>
          </a>
        </el-footer>
      </el-main>
    </el-container>
  </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script> <!-- Vue -->
<script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.3/lib/index.js"></script> <!-- ElementUI -->
<script>
  new Vue({
    el: '#app',
    data: {
      baseURL: document.location.origin,
      Number: 0,
      fileConfig: {
        image: {
          name: '图片',
          exts: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'ico'],
          icon: 'fas fa-image',
          count: 0
        },
        video: {
          name: '视频',
          exts: ['mp4', 'webm', 'ogg', 'avi', 'mov', 'wmv', 'flv', 'mkv'],
          icon: 'fas fa-video',
          count: 0
        },
        audio: {
          name: '音频',
          exts: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma'],
          icon: 'fas fa-music',
          count: 0
        },
        document: {
          name: '文件',
          exts: [],
          icon: 'fas fa-folder-open',
          count: 0
        }
      },
      uploadConfig: {
        maxSize: 20 * 1024 * 1024,  // 最大上传20MB
        maxConcurrent: 3
      },
        tableData: [],
        nextCursor: null,
      search: '',
      currentPage: 1,
      pageSize: 15,
      selectedFiles: [],
      sortOption: 'dateDesc',
      filterOption: 'all',
      fileType: 'image',
      quickWebsites: [
        { name: '原版后台', url: './admin.html', icon: 'fas fa-suitcase' },
        { name: '瀑布流', url: './admin-waterfall.html', icon: 'fas fa-wind' },
        { name: 'Movavi', url: 'https://www.movavi.com/zh/movavi-video-converter.html', icon: 'fas fa-file-video' },
        { name: 'FreeConvert', url: 'https://www.freeconvert.com/zh/video-compressor', icon: 'fas fa-file' },
        { name: 'YouCompress', url: 'https://www.youcompress.com/zh-cn/', icon: 'fas fa-file-zipper' },
        { name: 'Cloudinary', url: 'https://console.cloudinary.com/', icon: 'fas fa-cloud' },
      ],
    },
    computed: {
      filteredTableData() {
        return this.tableData.filter(data => {
          // 搜索匹配
          const searchLower = this.search.toLowerCase();
          const matchesSearch = !searchLower || [
            (data.metadata.fileName || '').toLowerCase(),
            data.name?.toLowerCase(),
          ].some(field => field?.includes(searchLower));

          // 筛选匹配
          const matchesFilter = {
            'all': true,
            'favorites': data.metadata.liked,
            'blocked': data.metadata.ListType === 'Block',
            'unblocked': data.metadata.ListType === 'White',
            'adult': data.metadata.Label?.toLowerCase() === 'adult',
          }[this.filterOption] ?? true;

          // 文件类型匹配
          const ext = data.name.split('.').pop().toLowerCase();
          const matchesType = this.fileType === 'document' ?
          !Object.keys(this.fileConfig).some(type =>
            type !== 'document' && this.fileConfig[type].exts.includes(ext) // 匹配其他所有文件
          ) :
          this.fileConfig[this.fileType].exts.includes(ext);
          return matchesSearch && matchesFilter && matchesType;
        });
      },
      paginatedTableData() {
        return this.sortData(this.filteredTableData)
          .slice((this.currentPage - 1) * this.pageSize, this.currentPage * this.pageSize);
      },
      sortIcon() { return `fas fa-sort-${this.sortOption === 'dateDesc' ? 'amount-down' : 'alpha-up'}`; },
      filterIcon() {
        return this.filterOption === 'all' ? 'fas fa-filter' :
        this.filterOption === 'favorites' ? 'fas fa-bookmark' :
        this.filterOption === 'blocked' ? 'fas fa-lock' :
        this.filterOption === 'unblocked' ? 'fas fa-unlock' :
        this.filterOption === 'adult' ? 'fas fa-user-secret' : '';
      },
      fileTypeIcon() {
        return this.fileType === 'image' ? 'fas fa-image' :
        this.fileType === 'video' ? 'fas fa-video' :
        this.fileType === 'audio' ? 'fas fa-music' :
        this.fileType === 'document' ? 'fas fa-folder-open' : '';
      }
    },
    watch: {  // 监听数据变化
      tableData: {
        handler(newData) {
          this.selectedFiles = newData.filter(file => file.selected);
        },
        deep: true
      },
      sortOption(newOption) { localStorage.setItem('sortOption', newOption); },
      filterOption(newOption) { localStorage.setItem('filterOption', newOption); }
    },
    methods: {
      refreshDashboard() {location.reload();},  // 刷新页面
      handleLogout() { window.location.href = '/'; },  // 退出登录
      handlePageChange(page) { this.currentPage = page; },  // 切换页面
        handleUpload() { this.$refs.fileInput.click(); },  // 打开文件选择对话框
        sort(command) { this.sortOption = command; },  // 切换排序方式
        filter(command) { this.filterOption = command; },  // 切换筛选方式
        loadMore() {
          const opts = { method: 'GET', credentials: 'include' };
          const url = this.nextCursor
            ? `./api/manage/list?cursor=${encodeURIComponent(this.nextCursor)}`
            : `./api/manage/list?limit=100`;

          fetch(url, opts)
            .then(r => r.json())
            .then(result => {
              const files = result.keys || [];
              const mapped = files.map(file => ({
                ...file,
                selected: false,
                metadata: {
                  ...file.metadata,
                  liked: file.metadata.liked ?? false,
                  fileName: file.metadata.fileName ?? file.name,
                  fileSize: file.metadata.fileSize ?? 0
                }
              }));
              this.tableData = this.tableData.concat(mapped);
              this.nextCursor = result.list_complete ? null : result.cursor;
              this.updateStats();
            })
            .catch(() => this.$message.error('同步数据时出错，请检查网络连接'));
        },
        sortData(data) {
          return this.sortOption === 'nameAsc' ? data.sort((a, b) => a.name.localeCompare(b.name)) :
            this.sortOption === 'sizeDesc' ? data.sort((a, b) => b.metadata.fileSize - a.metadata.fileSize) :
            data.sort((a, b) => b.metadata.TimeStamp - a.metadata.TimeStamp);
      },
      formattedFileDetails(item) {
        const metadata = item.metadata;
        const timestamp = new Date(metadata.TimeStamp).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
        return `
          <div style="text-align: left; padding: 5px;">
            <div><strong>ID：</strong>${item.name}</div>
            <div><strong>文件名：</strong>${metadata.fileName || item.name}</div>
            <div><strong>上传时间：</strong>${timestamp}</div>
            <div><strong>元数据：</strong>${JSON.stringify(metadata)}</div>
          </div>
        `;
      },
      calculatePageSize() {  // 设置页面大小
        const config = {
          minSize: 15, // 最小页面卡片数
          cardWidth: 240,
          ratio: 3/4, // 卡片高宽比
          gap: 20, // 卡片间距
          defaultWidth: 800,
          defaultHeaderHeight: 60 // 默认Dashboard高度
        };
        // 获取容器尺寸
        const content = document.querySelector('.content');
        const header = document.querySelector('.header-content');
        const width = content?.clientWidth || config.defaultWidth;
        const height = window.innerHeight - (header?.offsetHeight || config.defaultHeaderHeight);
        // 计算行列数
        const cols = Math.floor(width / (config.cardWidth + config.gap));
        const cardHeight = (width / cols - config.gap) * config.ratio;
        const rows = Math.floor(height / (cardHeight + config.gap));
        // 设置页面大小
        this.pageSize = Math.max(rows * cols, config.minSize);
      },
      updateWindowWidth() {  // 动态调整页面大小
        this.windowWidth = window.innerWidth;
        this.calculatePageSize();
      },
      updateStats() {
        this.Number = this.tableData.length;
        let fileCount = {image: 0, video: 0, audio: 0, document: 0};
        this.tableData.forEach(file => {
          const ext = file.name.split('.').pop().toLowerCase();
          const type = Object.keys(this.fileConfig).find(t =>
            this.fileConfig[t].exts.includes(ext)
          ) || 'document';
          fileCount[type]++;
        });
        Object.keys(this.fileConfig).forEach(type => {
          this.fileConfig[type].count = fileCount[type] || 0;
        });
      },
      // 文件操作
      async uploadFiles(event) {
        const files = Array.from(event.target.files || []);
        if (!files.length) return;
        // 文件验证配置
        // telegram-bot可分发的最大文件大小（20MB）
        // 大于20MB虽然能够成功上传，但无法获取文件链接
        const config = this.uploadConfig;
        // 过滤有效文件
        const valid = [], invalid = [];
        files.forEach(file => {
          (file.size <= config.maxSize ? valid : invalid).push(file);
        });
        // 显示错误信息
        if(invalid.length) { this.$message.error(`文件超过${config.maxSize/1024/1024}MB: \n${invalid.map(f => f.name).join('\n')}`); }
        if(!valid.length) { this.$message.info('没有符合条件的文件'); event.target.value = ''; return; }

        try {
          await this.$confirm(`确定要上传这 ${valid.length} 个文件吗?`, '提示', {
            type: 'warning'
          });
          const loading = this.$message({ message: '上传中...', duration: 0 });
          let [successCount, failed] = [0, []];
          // 并发上传处理函数
          const upload = async file => {
            try {
              const formData = new FormData();
              formData.append('file', file);
              const res = await fetch(`${this.baseURL}/upload`, {
                method: 'POST',
                body: formData,
                credentials: 'include'
              });
              const data = await res.json();
              if (!res.ok || (Array.isArray(data) && data[0]?.error)) {
                throw new Error(data[0]?.error || '上传失败');
              }
              const src = data[0]?.src;
              if (!src) throw new Error('未返回文件路径');
              // 验证并添加到列表
              const preview = await fetch(`${this.baseURL}${src}`, {
                credentials: 'include'
              });
              if (preview.ok) {
                this.tableData.unshift({
                  name: src.replace(/^\/file\//, ''),
                  selected: false,
                  metadata: { TimeStamp: Date.now(), fileSize: file.size, fileName: file.name }
                });
                successCount++;
              }
            } catch (err) {
              failed.push(`${file.name} (${err.message})`);
              console.error('Upload error:', err);
            }
          };
          // 分批上传
          for (let i = 0; i < valid.length; i += config.maxConcurrent) {
            await Promise.all(
              valid.slice(i, i + config.maxConcurrent).map(upload)
            );
          }
          loading.close();
          successCount && this.$message.success(`成功上传 ${successCount} 个文件`);
          failed.length && this.$message.error(`上传失败: ${failed.join(', ')}`);
          this.refreshFileList();

        } catch {
          this.$message.info('已取消上传');
        }
        event.target.value = '';
      },
        refreshFileList() {  // 不刷新页面,仅更新数据
          fetch("./api/manage/list?limit=100", { method: 'GET', credentials: 'include' })
            .then(response => response.json())
            .then(result => {
              const files = result.keys || [];
              this.tableData = files.map(file => ({
                ...file,
                selected: false,
                metadata: {
                  ...file.metadata,
                  liked: file.metadata.liked ?? false,
                  fileName: file.metadata.fileName ?? file.name,
                  fileSize: file.metadata.fileSize ?? 0
                }
              }));
              this.nextCursor = result.list_complete ? null : result.cursor;
              this.updateStats();
              this.sortData(this.tableData);
            })
            .catch(() => this.$message.error('刷新文件列表失败，请检查网络连接'));
        },
      toggleSelect(index, name) {
        const fileIndex = this.tableData.findIndex(file => file.name === name);
        this.tableData[fileIndex].selected = !this.tableData[fileIndex].selected;
      },
      toggleLike(index, name) {
        console.log(`Toggling like for : ${name}`);
        const fileIndex = this.tableData.findIndex(file => file.name === name);
        // 乐观更新收藏状态
        this.tableData[fileIndex].metadata.liked = !(this.tableData[fileIndex].metadata.liked ?? false);
        // 发送请求更新服务器数据
        var requestOptions = { method: 'GET', redirect: 'follow', credentials: 'include' };
        fetch(`./api/manage/toggleLike/${name}`, requestOptions)
          .then(response => response.json())
          .then(result => {
            if (!result.success) {  // 如果服务器更新失败，将状态还原
              this.tableData[fileIndex].metadata.liked = !this.tableData[fileIndex].metadata.liked;
              this.$message({message: '更新收藏状态失败，请稍后重试', type: 'error'});
            } else {
              this.$message.success(this.tableData[fileIndex].metadata.liked ? '收藏成功' : '取消收藏');
            }
          })
          .catch(error => { // 如果服务器响应错误，将状态还原
            this.tableData[fileIndex].metadata.liked = !this.tableData[fileIndex].metadata.liked;
            this.$message({message: '同步服务器失败，请检查网络连接', type: 'error'});
          });
      },
      handleDelete(index, key) {
        this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          fetch(`./api/manage/delete/${key}`, { method: 'GET', credentials: 'include' })
            .then(response => response.ok ? this.tableData.splice(index, 1) : Promise.reject())
            .then(() => {
              this.updateStats();
              this.$message.success('删除成功！');
            })
            .catch(() => this.$message.error('删除失败，请检查网络连接'));
        }).catch(() => this.$message.info('已取消删除'));
      },
      copyToClipboardFallback(text) {
        const textarea = document.createElement('textarea');
        document.body.appendChild(textarea);
        textarea.style.position = 'fixed';
        textarea.style.clip = 'rect(0 0 0 0)';
        textarea.style.top = '10px';
        textarea.value = text;
        textarea.select();
        document.execCommand('copy');
        document.body.removeChild(textarea);
      },
      handleCopy(index, key) {
        const link = `${this.baseURL}/file/${key}`;
        (navigator.clipboard?.writeText(link) || this.copyToClipboardFallback(link))
          .then(() => this.$message.success('复制文件链接成功~'))
          .catch(() => this.$message.error('自动复制失败，请手动复制链接：' + link));
      },
      // 批处理相关
      selectAllInPage() {  // 全选当前页
        const selected = !this.paginatedTableData.every(file => file.selected);
        this.paginatedTableData.forEach(file => file.selected = selected);
      },
      handleBatchDelete() {  // 批量删除
        this.$confirm('此操作将永久删除这 ' + this.selectedFiles.length + ' 个文件, 是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          const promises = this.selectedFiles.map(file => fetch(`./api/manage/delete/${file.name}`, { method: 'GET', credentials: 'include' }));

          Promise.all(promises)
            .then(results => {
              results.forEach((response, index) => {
                if (response.ok) {
                  const fileIndex = this.tableData.findIndex(file => file.name === this.selectedFiles[index].name);
                  if (fileIndex !== -1) {
                    this.tableData.splice(fileIndex, 1);
                  }
                }
              });
              this.selectedFiles = [];
              this.updateStats();
              this.$message.success('批量删除成功!');
            })
            .catch(() => this.$message.error('批量删除失败，请检查网络连接'));
        }).catch(() => this.$message.info('已取消批量删除'));
      },
      handleBatchCopy() {  // 批量复制链接
        const links = this.selectedFiles.map(file => `${document.location.origin}/file/${file.name}`).join('\n');
        (navigator.clipboard?.writeText(links) || this.copyToClipboardFallback(links))
          .then(() => this.$message.success('批量复制链接成功~'));
      },
      handleBatchDownload() {  // 批量下载
        this.$message.info(`正在下载 ${this.selectedFiles.length} 个文件`, { duration: 1000 });
        this.selectedFiles.forEach((file, index) => {
          setTimeout(() => {
            const link = document.createElement('a');
            link.href = `/file/${file.name}`;
            link.download = file.metadata.fileName || file.name;
            link.click();
          }, index * 800);
        });
        this.selectedFiles = [];
      },
      handleBatchBlockOrUnblock(type) {  // 批量加入黑/白名单
        if (type !== 'Block' && type !== 'White') { this.$message.error('无效的操作类型'); return; }
        const typeToName = { Block: '黑名单', White: '白名单' };
        this.$confirm(`确定要将这 ${this.selectedFiles.length} 个文件加入${typeToName[type]}吗?`, '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          const promises = this.selectedFiles.map(file => fetch(`./api/manage/${type.toLowerCase()}/${file.name}`, { method: 'GET', credentials: 'include' }));

          Promise.all(promises)
            .then(responses => {
              responses.forEach((response, index) => {
                if (response.ok) {
                  const fileIndex = this.tableData.findIndex(item => item.name === this.selectedFiles[index].name);
                  if (fileIndex !== -1) {
                    this.tableData[fileIndex].metadata.ListType = type;
                  }
                }
              });
              this.$message.success(`批量加入${typeToName[type]}成功`);
              this.selectedFiles = [];
            })
            .catch(() => this.$message.error(`操作失败，请检查网络连接`));
        });
      },
      handleBatchOperation(command) {
        if (this.selectedFiles.length === 0) { this.$message.warning('请先选择文件'); return; }
        switch (command) {
          case 'copy': this.handleBatchCopy(); break;
          case 'delete': this.handleBatchDelete(); break;
          case 'download': this.handleBatchDownload(); break;
          case 'block': this.handleBatchBlockOrUnblock('Block'); break;
          case 'unblock': this.handleBatchBlockOrUnblock('White'); break;
        }
      },
      // 工具相关
      handleToolkit(command) {
        switch (command) {
          case 'selectAllInPage': this.selectAllInPage(); break;
          case 'checkBrokenFiles': this.checkBrokenFiles(); break;
          case 'editWebsites': this.editWebsites(); break;
        }
      },
      switchFileType(type) {  // 切换文件类型
        this.fileType = type;
        this.currentPage = 1;
        localStorage.setItem('fileType', type);
        this.$message({
          message: `已切换为${this.fileConfig[type].name}模式, 共${this.fileConfig[type].count}个文件`,
          type: 'success',
          duration: 1500
        });
      },
      checkBrokenFiles() {  // 检测失效文件
        const loadingMessage = this.$message({ message: '正在检测失效文件...', duration: 0});

        let brokenCount = 0;
        const promises = this.tableData.map((item, index) => {
          const fileIndex = this.tableData.findIndex(item => item.name === this.selectedFiles[index].name);
          return new Promise((resolve) => {
            fetch(`${this.baseURL}/file/${item.name}`, {
              method: 'HEAD',
              cache: 'no-cache'  // 避免缓存影响检测结果
            })
              .then(response => {
                if (!response.ok) {
                  brokenCount++;
                  this.tableData[fileIndex].selected = true;
                  resolve({ index, status: 'error' });
                } else {
                  resolve({ index, status: 'success' });
                }
              })
              .catch(() => {
                brokenCount++;
                this.tableData[fileIndex].selected = true;
                resolve({ index, status: 'error' });
              });
          });
        });

        Promise.all(promises).then(() => {
          loadingMessage.close();
          if (brokenCount > 0) {
            this.$message({
              dangerouslyUseHTMLString: true,
              message: `检测到 ${brokenCount} 个失效文件，已自动选中。<br>您可以使用批量删除功能移除它们。`,
              type: 'warning',
              duration: 5000
            });
          } else {
            this.$message({
              message: '未检测到失效文件',
              type: 'success'
            });
          }
        });
      },
      // 处理网站点击
      handleWebsite(url) { window.open(url, '_blank'); },
      // 编辑快捷方式
      editWebsites() {
        const websiteText = this.quickWebsites
          .map(site => `${site.name}|${site.url}|${site.icon}`)
          .join('\n');

        this.$prompt('', '编辑快捷方式', {
          inputType: 'textarea',
          inputValue: websiteText,
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          dangerouslyUseHTMLString: true,
          customClass: 'website-edit-dialog',
          message: `
            <div>
              每行一个网站，格式：名称|网址|图标类名<br>
              图标需在 <a href="https://fontawesome.com/v6/search?m=free" target="_blank" style="color: #409EFF; text-decoration: underline">Font Awesome</a> 中选择
            </div>
          `,
          inputValidator: (value) => {
            const lines = value.split('\n');
            if (lines.length > 10) return '不能超过10行';
            for (let line of lines) {
              if (!line.trim()) continue;
              const [name, url, icon = 'fas fa-link'] = line.split('|');
              if (!name || !url) return '名称和网址不能为空：' + line;
              if (!(/^https?:\/\/.+/.test(url) || /^\.\/.*/.test(url))) return '网址格式错误：'+line;
              if (!/^(fas?|fa-brands|fa-regular|fa-solid)\s+fa-[a-z0-9-]+$/.test(icon)) {
                return '图标类名格式错误：' + line;
              }
            }
            return true;
          }
        }).then(({ value }) => {
          const newSites = value.split('\n')
            .filter(line => line.trim())
            .map(line => {
              const [name, url, icon = 'fas fa-link'] = line.split('|');
              return { name, url, icon };
            });
          this.quickWebsites = newSites;
          localStorage.setItem('quickWebsites', JSON.stringify(newSites));
          this.$message.success('保存成功');
        }).catch(() => {});
      },
      getFileIcon(filename) {
        const ext = filename.split('.').pop().toLowerCase();
        const iconMap = {
          'pdf': 'fas fa-file-pdf',
          'doc,docx': 'fas fa-file-word',
          'xls,xlsx,csv': 'fas fa-file-excel',
          'ppt,pptx': 'fas fa-file-powerpoint',
          'txt,md,log': 'fas fa-file-lines',
          'zip,rar,7z,tar,gz': 'fas fa-file-zipper',
          'html,htm,css,js,ts,jsx,tsx,vue,php,py,java,c,cpp,h,hpp,cs,go,rs,rb,pl,sh,sql': 'fas fa-file-code',
          'json,xml,yaml,yml,toml': 'fas fa-file-code',
          'mp4,avi,mov,wmv,flv,mkv,webm': 'fas fa-file-video',
          'mp3,wav,ogg,flac,aac,m4a,wma': 'fas fa-file-audio',
          'jpg,jpeg,png,gif,bmp,webp,svg,ico': 'fas fa-file-image',
          'psd,ai,eps,cdr': 'fas fa-file-image',
          'exe,msi,app,dmg,deb,rpm': 'fas fa-file-arrow-down'
        };
        for(const [exts, icon] of Object.entries(iconMap)) {
          if(exts.split(',').includes(ext)) {
            return icon;
          }
        }
        return 'fas fa-file';
      },
      getFileType(filename) {
        const ext = filename.split('.').pop();
        return `${ext.toUpperCase()}`;
      },
      handleEditName(item) {
        this.$prompt('', '修改文件名', {
          inputValue: item.metadata?.fileName || item.name,
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          inputValidator: (value) => {
            if (!value) return '文件名不能为空';
            if (value.length > 64) return '文件名不能超过64个字符';
            return true;
          }
        }).then(({ value }) => {
          fetch(`./api/manage/editName/${item.name}?newName=${encodeURIComponent(value)}`, {
            method: 'GET',
            credentials: 'include'
          })
          .then(response => response.json())
          .then(result => {
            if (result.success) {
              item.metadata.fileName = value;
              this.$message.success('文件名修改成功');
            } else {
              this.$message.error('文件名修改失败');
            }
          })
          .catch(() => this.$message.error('修改文件名时出错，请检查网络连接'));
        }).catch(() => {});
      }
    },
    mounted() {
      window.addEventListener('resize', this.calculatePageSize);
      this.updateWindowWidth();
      fetch("./api/manage/check", { method: 'GET', credentials: 'include' })
        .then(response => response.text())
        .then(result => result === "true" ? this.showLogoutButton = true : window.location.href = "./api/manage/login")
        .catch(() => this.$message.error('同步数据时出错，请检查网络连接'));

      // 获取文件列表数据
        fetch("./api/manage/list?limit=100", { method: 'GET', credentials: 'include' })
          .then(response => response.json())
          .then(result => {
            console.log("result: ", result);
            const files = result.keys || [];
            this.nextCursor = result.list_complete ? null : result.cursor;
            this.tableData = files.map(file => ({
              ...file,
              selected: false,
              metadata: {
                ...file.metadata,
                liked: file.metadata.liked ?? false,
                fileName: file.metadata.fileName ?? file.name,
                fileSize: file.metadata.fileSize ?? 0
              }
            }));
            this.updateStats();
            // 恢复设置
            this.sortOption = localStorage.getItem('sortOption') || this.sortOption;
            this.sortData(this.tableData);
            this.fileType = localStorage.getItem('fileType') || this.fileType;
            this.switchFileType(this.fileType);
          })
          .catch(() => this.$message.error('同步数据时出错，请检查网络连接'));

      if (localStorage.getItem('quickWebsites')) {
        this.quickWebsites = JSON.parse(localStorage.getItem('quickWebsites'));
      }
    },
    beforeDestroy() {
      window.removeEventListener('resize', this.calculatePageSize);
    }
  });
</script>
</html>